// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: qiang

#include "coin2/exchange/apollox/api_order/native_private_client.h"

#include <list>
#include <memory>
#include <sstream>
#include <string>

#include "coin2/exchange/apollox/symbology/product.h"
#include "coin2/exchange/apollox_base/api_util/rate_tracker.h"
#include "coin2/exchange/base/api_util/url_params_builder.h"

using coin2::exchange::apollox::symbology::ApolloxProduct;
using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;

#define CHECK_API_LIMIT(action_type)                                                      \
  if (!HasEnoughRateLimit(ts, action_type)) {                                             \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }

namespace {
using ActionType = apollox::api_util::ApolloxRateTracker::ActionType;
using coin2::exchange::apollox::api_util::ApolloxRateTracker;
}  // namespace

auto ApolloxNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  ApolloxProduct product =
      ApolloxProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  params.set_symbol(order_spec.product);
  auto formatter = product_holder->product_info().order_formatter();
  if (order_spec.order_type != OrderType::MARKET_ORDER) {
    params.set_price(formatter.FormatPrice(order_spec.price));
  }
  params.set_quantity(formatter.FormatQty(order_spec.qty));
  params.set_new_client_order_id(std::to_string(proc_order_id));
  params.set_recv_window(5000);

  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_type("MARKET");
  } else if (order_spec.order_type == OrderType::LIMIT_ORDER) {
    params.set_type("LIMIT");
  } else {
    NOTREACHED() << "Unsupported order type: " << static_cast<int>(order_spec.order_side);
  }

  if (order_spec.post_only || order_spec.duration == coin::proto::OrderDuration::GTX_ORDER) {
    params.set_time_in_force("GTX");
  } else if (order_spec.duration == coin::proto::OrderDuration::GTC_ORDER) {
    params.set_time_in_force("GTC");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_time_in_force("IOC");
  } else if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_time_in_force("FOK");
  } else {
    NOTREACHED() << "Unsupported duration type: " << static_cast<int>(order_spec.duration);
  }

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("BUY");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("SELL");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  return params;
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::GenListenKey() {
  auto ts = GetCurrentTimestamp();
  // CHECK_API_LIMIT(ActionType::QUERY);

  UrlParamsBuilder builder;
  auto context = std::shared_ptr<AsioHttpContext>(
      GetFirstAuth()->CreateRequestByPostWithSignature("/api/v1/listenKey", &builder));
  SetBindIp(context->bind_ip);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::LISTEN_KEY;
  context->extra_info = DebugString();
  return context;
}

auto ApolloxNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  const std::string& product_str = order_context.order_spec().product;
  params.set_symbol(product_str);
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id().substr(product_str.length()));
  } else {
    params.set_orig_client_order_id(std::to_string(order_context.proc_order_id()));
  }

  params.set_recv_window(5000);
  return params;
}

ApolloxNativePrivateClient::ApolloxNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  using coin2::exchange::apollox::api_util::ApolloxRateTracker;
  NativeClientConfig config;
  config.set_rest_host("www.apollox.finance");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[ApolloxNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<ApolloxRestAuth, NativeClientConfig>(config);
  rate_tracker_ = std::make_unique<ApolloxRateTracker>(mea_, config);
  start_time_ = GetCurrentTimestamp();
}

BaseRestAuth* ApolloxNativePrivateClient::GetAuth() const {
  return auths_.GetRandomTryNotUseFirst();
}

void ApolloxNativePrivateClient::SetBindIp(const std::string& ip) {
  static_cast<ApolloxRateTracker*>(rate_tracker_.get())->SetBindIp(ip);
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::QueryAccountBalance(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api/v1/account", &builder));
  SetBindIp(context->bind_ip);

  // query balance may be blocked due to rate limit consumed by IP,
  // which blocks system be ready
  if (!is_first_query_account_balance_) {
    CHECK_API_LIMIT(ActionType::QUERY);
  }
  is_first_query_account_balance_ = false;

  Push(5, ts, ActionType::QUERY);
  api_req_info_[__func__]++;
  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::QueryOpenOrders(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  std::string symbol = "";
  int weight = 40;
  UrlParamsBuilder builder;
  if (params.has_symbol() && !params.symbol().empty()) {
    builder.PutGet("symbol", params.symbol());
    symbol = params.symbol();
    weight = 1;
  }
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api/v1/openOrders", &builder));
  SetBindIp(context->bind_ip);

  if (query_open_orders_ts_map_.count(symbol) &&
      ts < query_open_orders_ts_map_[symbol] + 120'000'000'000LL /* 2mins */) {
    CHECK_API_LIMIT(ActionType::QUERY);
  }
  query_open_orders_ts_map_[symbol] = ts;

  Push(weight, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::QueryAllOrders(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  builder.PutGet("limit", params.limit());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api/v1/allOrders", &builder));
  SetBindIp(context->bind_ip);

  CHECK_API_LIMIT(ActionType::QUERY);

  Push(5, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::QueryTrades(const RestParams& params) {
  DLOG_EVERY_N(INFO, 50) << DebugString();
  auto ts = GetCurrentTimestamp();

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  builder.PutGet("limit", params.limit());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api/v1/userTrades", &builder));
  SetBindIp(context->bind_ip);

  CHECK_API_LIMIT(ActionType::QUERY);

  Push(5, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::PlaceOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("side", params.side());
  builder.PutGet("type", params.type());
  builder.PutGet("timeInForce", params.time_in_force());
  builder.PutGet("quantity", params.quantity());
  builder.PutGet("price", params.price());
  builder.PutGet("newClientOrderId", params.new_client_order_id());
  builder.PutGet("stopPrice", params.stop_price());  // not used
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetFirstAuth()->CreateRequestByPostWithSignature("/api/v1/order", &builder));
  SetBindIp(context->bind_ip);
  CHECK_API_LIMIT(ActionType::PLACE);
  Push(1, ts, ActionType::PLACE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::CancelOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("orderId", params.order_id());
  builder.PutGet("origClientOrderId", params.orig_client_order_id());
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetFirstAuth()->CreateRequestByDeleteWithSignature("/api/v1/order", &builder));
  SetBindIp(context->bind_ip);

  CHECK_API_LIMIT(ActionType::CANCEL);

  Push(1, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  LOG(INFO) << "order_exe " << order_exe.proc_order_id << "===1" << std::endl;
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

void ApolloxNativePrivateClient::Prepare() {
  auto* tracker = dynamic_cast<ApolloxRateTracker*>(this->rate_tracker_.get());
  mb_->Subscribe("apollox/req_rate_limit", [this, tracker](const std::string&, void* data) {
    auto json = nlohmann::json::parse(*reinterpret_cast<std::string*>(data));
    tracker->UpdateReqRateInfo(json);
  });
}

std::string ApolloxNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;

  std::stringstream ss;
  ss << "ApolloxNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> ApolloxNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    default: {
      return nullptr;
    }
  }
}

AsioHttpContextList ApolloxNativePrivateClient::SubmitRestQueryList(const RestQueryProto& query) {
  AsioHttpContextList list;
  switch (query.type()) {
    case RestQueryProto::QUERY_TRANSFERS: {
      break;
    }
    default: {
      return list;
    }
  }

  return list;
}